﻿/*
 *  This file is part of SXAPI.
 *
 *  SXAPI is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, version 3 of the License.

 *  SXAPI is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *  
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with SXAPI.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.Reflection;

namespace StackExchangeApi
{

    /// <summary>
    /// Pair utlity class
    /// </summary>
    /// <typeparam name="T">The first element type of the Pair</typeparam>
    /// <typeparam name="V">The second element type of the Pair</typeparam>
    internal sealed class Pair<T, V>
    {
        /// <summary>
        /// The First element in the Pair
        /// </summary>
        public T First
        {
            get;
            set;
        }

        /// <summary>
        /// The Second element in the Pair
        /// </summary>
        public V Second
        {
            get;
            set;
        }

        /// <summary>
        /// Construct a new Pair
        /// </summary>
        /// <param name="first">The First element</param>
        /// <param name="second">The Second element</param>
        public Pair(T first, V second)
        {
            First = first;
            Second = second;
        }

        /// <summary>
        /// Custom equality operator
        /// </summary>
        /// <param name="obj">To compare with</param>
        /// <returns>true if equal, false otherwise</returns>
        public override bool Equals(object obj)
        {
            Pair<T, V> other = obj as Pair<T, V>;

            if (other == null) return false;

            return other.First.Equals(First) &&
                   other.Second.Equals(Second);
        }

        /// <summary>
        /// Custom hash code implementation
        /// </summary>
        /// <returns>A hashcode for this Pair</returns>
        public override int GetHashCode()
        {
            return First.GetHashCode() ^
                   (Second.GetHashCode() * -1);
        }

        #if DEBUG

        /// <summary>
        /// For debugging purposes only
        /// </summary>
        /// <returns>A string representation of this Pair</returns>
        public override string ToString()
        {
            return "<" + First + ", " + Second + ">";
        }
        #endif
    }

    /// <summary>
    /// Holds a few utility methods.
    /// Generally, a method only belongs here if its a few lines of code,
    /// static, and replicated (or potentially so) across many different
    /// classes.
    /// </summary>
    internal sealed class Utils
    {
        private static DateTime STACKOVERFLOW_LAUNCH = new DateTime(2008, 7, 31, 21, 26, 0, DateTimeKind.Utc); //July  31st, 2008 21:26
        private static DateTime SERVERFAULT_LAUNCH   = new DateTime(2009, 4, 30,  6, 49, 0, DateTimeKind.Utc); //April 30th, 2009  6:49
        private static DateTime META_LAUNCH          = new DateTime(2009, 6, 28,  7, 14, 0, DateTimeKind.Utc); //June  28th, 2009  7:14
        private static DateTime SUPERUSER_LAUNCH     = new DateTime(2009, 7, 15,  6, 27, 0, DateTimeKind.Utc); //July  15th, 2009  6:27

        /// <summary>
        /// Utility method for getting the address component of a trilogy website.
        /// 
        /// Ie. "meta.stackoverflow.com" or "serverfault.com"
        /// </summary>
        /// <param name="site">StackExchangeSite to convert</param>
        /// <returns>As above</returns>
        internal static String GetAddressForSite(StackExchangeSite site)
        {
            switch (site)
            {
                case StackExchangeSite.Meta: return "meta.stackoverflow.com";
                case StackExchangeSite.ServerFault: return "serverfault.com";
                case StackExchangeSite.StackOverflow: return "stackoverflow.com";
                case StackExchangeSite.SuperUser: return "superuser.com";
                default: throw new ArgumentException("Unknown site [" + Enum.GetName(typeof(StackExchangeSite), site) + "]");
            }
        }

        /// <summary>
        /// Utility method for converting a .NET DateTime into a unix timestamp
        /// </summary>
        /// <param name="date">DateTime to convert</param>
        /// <returns>A long that is a unix timestamp</returns>
        internal static long ToUnixTime(DateTime date)
        {
            return ((long)(date - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds) * 1000;
        }

        /// <summary>
        /// Gets the site a specific host maps to.
        /// Inverse of GetAddressForSite(...)
        /// 
        /// Throws an ArgumentException if the site is not known.
        /// </summary>
        /// <param name="host">Host string of a StackExchangeSite</param>
        /// <returns>A StackExchangeSite</returns>
        internal static StackExchangeSite GetSiteForAddress(string host)
        {
            foreach (StackExchangeSite site in Enum.GetValues(typeof(StackExchangeSite)))
            {
                if (host.Equals(GetAddressForSite(site)))
                    return site;
            }

            throw new ArgumentException(
                String.Format(
                CultureInfo.InvariantCulture,
                "Expected a host string returned by GetAddressForSite, found {0}", 
                host));
        }

        /// <summary>
        /// Returns the date the given StackExchangeSite "launched" on.
        /// No event could have occurred (no questions, no voting, etc.) prior
        /// to this launch date.
        /// </summary>
        /// <param name="site">The site in question</param>
        /// <returns>The DateTime the earliest question was asked</returns>
        internal static DateTime GetLaunchDateFor(StackExchangeSite site)
        {
            switch (site)
            {
                case StackExchangeSite.Meta: return META_LAUNCH;
                case StackExchangeSite.ServerFault: return SERVERFAULT_LAUNCH;
                case StackExchangeSite.StackOverflow: return STACKOVERFLOW_LAUNCH;
                case StackExchangeSite.SuperUser: return SUPERUSER_LAUNCH;
            }

            throw new ArgumentException(
                String.Format(
                CultureInfo.InvariantCulture,
                "Exception a known StackExchangeSite, found {0}",
                Enum.GetName(typeof(StackExchangeSite), site)
            ));
        }

        /// <summary>
        /// Loads all exposed properties of fromType on from to
        /// to, using toType to located the destination properties.
        /// 
        /// Used to effective eager-loading casts.
        /// </summary>
        /// <param name="from">Object to read properties from</param>
        /// <param name="to">Objects to set properties on</param>
        /// <param name="fromType">Type to use to discover properties on from</param>
        /// <param name="toType">Type to use to discover properties on to</param>
        internal static void EagerlyLoad(Object from, Object to, Type fromType, Type toType)
        {
            Type roDelayLoadedList = typeof(ReadOnlyDelayLoadedList<>);

            MethodInfo asEager = roDelayLoadedList.GetMethod("AsEager", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);

            PropertyInfo[] props = fromType.GetProperties();

            foreach (PropertyInfo prop in props)
            {
                PropertyInfo localProp = toType.GetProperty(prop.Name);

                Object value = prop.GetValue(from, null);

                if (value.GetType().IsSubclassOf(roDelayLoadedList) || value.GetType() == roDelayLoadedList)
                {
                    value = asEager.Invoke(value, new Object[0]);
                }

                localProp.SetValue(
                    to,
                    value,
                    null);
            }
        }

        /// <summary>
        /// Get the default value of a type, given its type.
        /// </summary>
        /// <param name="type">The Type in question</param>
        /// <returns>The equivalent default(Type)</returns>
        internal static object DefaultValue(Type type)
        {
            if (type.IsValueType)
            {
                return Activator.CreateInstance(type);
            }

            return null;
        }

        /// <summary>
        /// Implements standard ICache behavior.
        /// </summary>
        /// <param name="cached">The ICached implementing object to work with</param>
        internal static void ICachedImpl(ICached cached)
        {
            //Get all fields that are NOT public and NOT static
            Type type = cached.GetType();

            do
            {
                FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

                foreach (FieldInfo field in fields)
                {
                    foreach (Attribute at in field.GetCustomAttributes(true))
                        if (at is BacksCache)
                        {
                            field.SetValue(cached, Utils.DefaultValue(field.FieldType));
                        }
                }
            } while ((type = type.BaseType) != null);
        }
    }
}
